#ifndef F5472JIRAFUNC0V1_HH
#define F5472JIRAFUNC0V1_HH

#include "../F100/f100datafunc0v1.hh"
#include "../F100/f100ccssdefi0v1.hh"

struct jira_c;
struct jira_s;

#include <cstdlib>
#include <cstddef>
#include <ctime>
#include <cstdio>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>
#include <algorithm>
#include <chrono>
#include <pybind11/embed.h>
#include <pybind11/stl.h>

namespace py = pybind11;

struct jira_c
{
  std::string server = "https://its.cern.ch/jira";
  std::string token = "*****************************************"; // VITAL token
  std::string query = "project=PRCAMPAIGNS ORDER BY updated DESC, priority DESC";
  int timeout = 45;
  int max_results = 100000;
};

struct jira_s
{
  std::string key;
  std::string summary;
  std::string status;
  std::string priority;
  std::string updated;
  std::string created;
  std::string description;
  void print_() const
  {
    std::cout << "Key: " << key << std::endl;
    std::cout << "Summary: " << summary << std::endl;
    std::cout << "Status: " << status << std::endl;
    std::cout << "Priority: " << priority << std::endl;
    std::cout << "Updated: " << updated << std::endl;
    std::cout << "Created: " << created << std::endl;
    std::cout << "Description: " << description << std::endl;
    std::cout << std::string(45, '-') << std::endl;
  }
  size_t size_() const
  {
    return cc_size_(key) + cc_size_(summary) + cc_size_(status)
      + cc_size_(priority) + cc_size_(updated) + cc_size_(created) + cc_size_(description);
  }
  size_t serialize_(void* buffer) const
  {
    uint8_t* ptr = static_cast<uint8_t*>(buffer);
    size_t offset = 0;
    offset += cc_serialize_(key, ptr + offset);
    offset += cc_serialize_(summary, ptr + offset);
    offset += cc_serialize_(status, ptr + offset);
    offset += cc_serialize_(priority, ptr + offset);
    offset += cc_serialize_(updated, ptr + offset);
    offset += cc_serialize_(created, ptr + offset);
    offset += cc_serialize_(description, ptr + offset);
    return offset;
  }
  size_t deserialize_(const void* buffer)
  {
    const uint8_t* ptr = static_cast<const uint8_t*>(buffer);
    size_t offset = 0;
    offset += cc_deserialize_(key, ptr + offset);
    offset += cc_deserialize_(summary, ptr + offset);
    offset += cc_deserialize_(status, ptr + offset);
    offset += cc_deserialize_(priority, ptr + offset);
    offset += cc_deserialize_(updated, ptr + offset);
    offset += cc_deserialize_(created, ptr + offset);
    offset += cc_deserialize_(description, ptr + offset);
    return offset;
  }
};

inline int jira_parse_number_(const std::string &key) // "PRCAMPAIGNS-281" -> 281; 0 = not found
{
  size_t pos = key.find_last_of('-');
  if (pos != std::string::npos && pos + 1 < key.length())
  {
    try
    {
      return std::stoi(key.substr(pos + 1));
    }
    catch (...) { return 0; }
  }
  return 0;
}

inline std::vector<int> jira_parse_ranges_(const std::string& range_str) // "300-400,500,600-700" -> [300, 301, ..., 400, 500, 600, 601, ..., 700]; {} = input error
{
  std::vector<int> numbers;
  std::stringstream ss(range_str);
  std::string token;
  while (std::getline(ss, token, ','))
  {
    token.erase(token.begin(), std::find_if(token.begin(), token.end(), [](unsigned char ch)
    {
      return !std::isspace(ch);
    }));
    token.erase(std::find_if(token.rbegin(), token.rend(), [](unsigned char ch)
    {
      return !std::isspace(ch);
    }).base(), token.end());
    size_t dash_pos = token.find('-');
    if (dash_pos != std::string::npos)
    {
      try
      {
        int start = std::stoi(token.substr(0, dash_pos));
        int end = std::stoi(token.substr(dash_pos + 1));
        for (int i = start; i <= end; ++i)
        {
          numbers.push_back(i);
        }
      }
      catch (...) { numbers.clear(); std::cerr << "Invalid range: " << token << std::endl; }
    }
    else
    {
      try { numbers.push_back(std::stoi(token)); } catch (...) { numbers.clear(); std::cerr << "Invalid number: " << token << std::endl; }
    }
  }
  return numbers;
}

inline std::string jira_transition_sequence_(const std::string& current_status, const std::string& target_status)
{
  // Campaign ticket workflow:
  // To Do -> Open
  // Open -> Closed; Open -> Enabled
  // Enabled -> Open; Enabled -> Closed
  // Closed -> Open
  std::vector<std::string> sequence;
  if (current_status == "To Do" && target_status == "Open") sequence = {"Add Campaign"};
  else if (current_status == "To Do" && target_status == "Closed") sequence = {"Closed"};
  else if (current_status == "To Do" && target_status == "Enabled") sequence = {"Add Campaign", "Enable"};
  else if (current_status == "Open" && target_status == "Closed") sequence = {"Closed"};
  else if (current_status == "Open" && target_status == "Enabled") sequence = {"Enable"};
  else if (current_status == "Open" && target_status == "To Do") sequence = {"Unreachable"};
  else if (current_status == "Enabled" && target_status == "Open") sequence = {"Disable"};
  else if (current_status == "Enabled" && target_status == "Closed") sequence = {"Closed"};
  else if (current_status == "Enabled" && target_status == "To Do") sequence = {"Unreachable"};
  else if (current_status == "Closed" && target_status == "Open") sequence = {"Reopen"};
  else if (current_status == "Closed" && target_status == "Enabled") sequence = {"Reopen", "Enable"};
  else if (current_status == "Closed" && target_status == "To Do") sequence = {"Unreachable"};
  else sequence = {current_status + " => " + target_status}; // unknown transition for debugging
  if (sequence.empty()) return "Transition sequence void.";
  std::string result = sequence[0];
  for (size_t i = 1; i < sequence.size(); ++i)
  {
    result += ", " + sequence[i];
  }
  return result;
}

inline std::vector<std::string> jira_transition_parse_(const std::string& trans_sequence) // "To Do -> Open, Open -> Closed" -> ["To Do -> Open", "Open -> Closed"]; {} = input error
{
  std::vector<std::string> transitions;
  std::stringstream ss(trans_sequence);
  std::string transition;
  while (std::getline(ss, transition, ','))
  {
    transition.erase(transition.begin(), std::find_if(transition.begin(), transition.end(), [](unsigned char ch)
    {
      return !std::isspace(ch);
    }));
    transition.erase(std::find_if(transition.rbegin(), transition.rend(), [](unsigned char ch)
    {
      return !std::isspace(ch);
    }).base(), transition.end());
    if (!transition.empty()) transitions.push_back(transition);
  }
  return transitions;
}

std::vector<jira_s> jira_fetch_campaign_(const jira_c& config)
{
  std::vector<jira_s> jira_issues;
  py::scoped_interpreter guard{};
  try
  {
    // 1. python func fetch with pagination
    py::exec(R"XXXX(
from jira import JIRA # python -m venv a5472_venv / source a5472_venv/bin/activate / pip install jira / deactivate

def fetch_jira_issues_(server, token, query, timeout, max_results):
  jira = JIRA(server=server, token_auth=token, timeout=timeout)
  result = []
  batch_size = 100
  start_at = 0
  total_fetched = 0

  print(f"Fetching JIRA issues (max {max_results})...")
  while total_fetched < max_results:
    current_batch_size = min(batch_size, max_results - total_fetched)
    try:
      print(f"  Fetching batch: startAt={start_at}, maxResults={current_batch_size}...", end=' ', flush=True)
      issues = jira.search_issues(query, startAt=start_at, maxResults=current_batch_size)
      if not issues:
        print("(no more issues)")
        break
      for issue in issues:
        result.append({
          "key": issue.key,
          "summary": issue.fields.summary,
          "status": str(issue.fields.status),
          "priority": str(issue.fields.priority) if issue.fields.priority else 'None',
          "updated": issue.fields.updated,
          "created": issue.fields.created,
          "description": issue.fields.description if issue.fields.description else ''
        })
      total_fetched += len(issues)
      print(f"got {len(issues)} issues (total: {total_fetched})")
      if len(issues) < current_batch_size:
        break
      start_at += len(issues)
    except Exception as e:
      print(f"\nError fetching batch starting at {start_at}: {e}")
      break
  print(f"Finished fetching: {total_fetched} issues total")
  return result
)XXXX");
    // 2. python call fetch
    auto py_fetch_jira_issues_ = py::globals()["fetch_jira_issues_"];
    py::list py_jira_issues = py_fetch_jira_issues_(config.server, config.token, config.query, config.timeout, config.max_results);
    // 3. python dict to C++ struct
    for (auto item : py_jira_issues)
    {
      py::dict py_jira_dict = item.cast<py::dict>();
      jira_s jira_issue;
      jira_issue.key = py_jira_dict["key"].cast<std::string>();
      jira_issue.summary = py_jira_dict["summary"].cast<std::string>();
      jira_issue.status = py_jira_dict["status"].cast<std::string>();
      jira_issue.priority = py_jira_dict["priority"].cast<std::string>();
      jira_issue.updated = py_jira_dict["updated"].cast<std::string>();
      jira_issue.created = py_jira_dict["created"].cast<std::string>();
      jira_issue.description = py_jira_dict["description"].cast<std::string>();
      jira_issues.push_back(jira_issue);
    }
  }
  catch (const py::error_already_set &e)
  {
    std::cerr << "Python error: " << e.what() << std::endl;
  }
  return jira_issues;
}

int jira_execute_transitions_(const std::vector<std::string>& issue_keys
  , const std::unordered_map<std::string, std::pair<std::string, std::string>>& jira_status_map
  , const jira_c& config
)
{
  py::scoped_interpreter guard{};
  try
  {
    std::string python_code = R"XXXX(
from jira import JIRA # python -m venv a5472_venv / source a5472_venv/bin/activate / pip install jira / deactivate

jira_client = JIRA(server=")XXXX" + config.server + R"XXXX(", token_auth=")XXXX" + config.token + R"XXXX(", timeout=)XXXX" + std::to_string(config.timeout) + R"XXXX()

def execute_transitions_(issue_key, transitions):
  issue = jira_client.issue(issue_key)
  for transition in transitions:
    transition_id = None
    available_transitions = jira_client.transitions(issue)
    for t in available_transitions:
      if t['name'].lower() == transition.lower():
        transition_id = t['id']
        break
    if transition_id:
      jira_client.transition_issue(issue, transition_id)
      print(f"Executed: {issue_key} -> {transition}")
      issue = jira_client.issue(issue_key) # refresh issue
    else:
      available_names = [t['name'] for t in available_transitions]
      raise ValueError(f"Transition '{transition}' not found for {issue_key}. Available: {available_names}")
  return True
)XXXX";
    py::exec(python_code);
    auto py_execute_transitions_ = py::globals()["execute_transitions_"];
    size_t executed_count = 0;
    for (const std::string& issue_key : issue_keys)
    {
      auto status_it = jira_status_map.find(issue_key);
      if (status_it == jira_status_map.end())
      {
        std::cerr << "Issue " << issue_key << " not found in status map" << std::endl;
        continue;
      }
      const std::string& cur_status = status_it->second.first;
      const std::string& should_be_status = status_it->second.second;
      std::string trans_sequence = jira_transition_sequence_(cur_status, should_be_status);
      std::vector<std::string> transitions = jira_transition_parse_(trans_sequence);
      if (!transitions.empty() && transitions[0] != "Unreachable")
      {
        try
        {
          bool success = py_execute_transitions_(issue_key, transitions).cast<bool>();
          if (success) executed_count++;
        }
        catch (const py::error_already_set &e)
        {
          std::cerr << "Failed to execute transitions for " << issue_key << ": " << e.what() << std::endl;
        }
      }
      else
      {
        std::cerr << "Skipping " << issue_key << ": " << trans_sequence << std::endl;
      }
    }
    std::cout << "\nExecuted transitions for " << executed_count << " / " << issue_keys.size() << " issues" << std::endl;
    return 0;
  }
  catch (const py::error_already_set &e)
  {
    std::cerr << "Python error: " << e.what() << std::endl;
    return 1;
  }
}

inline std::string campaign_remove_secondary_(const std::string& text)
{
  std::string result;
  result.reserve(text.length());
  size_t i = 0;
  while (i < text.length())
  {
    // Check if we find secondary dataset patterns starting with /
    size_t secondary_pos = std::string::npos;
    // Check for /Neutrino
    size_t neutrino_pos = text.find("/Neutrino", i);
    if (neutrino_pos != std::string::npos && (secondary_pos == std::string::npos || neutrino_pos < secondary_pos))
      secondary_pos = neutrino_pos;
    // Check for /MinBias
    size_t minbias_pos = text.find("/MinBias", i);
    if (minbias_pos != std::string::npos && (secondary_pos == std::string::npos || minbias_pos < secondary_pos))
      secondary_pos = minbias_pos;
    // Check for /Hydjet
    size_t hydjet_pos = text.find("/Hydjet", i);
    if (hydjet_pos != std::string::npos && (secondary_pos == std::string::npos || hydjet_pos < secondary_pos))
      secondary_pos = hydjet_pos;
    // Check for /RelVal
    size_t relval_pos = text.find("/RelVal", i);
    if (relval_pos != std::string::npos && (secondary_pos == std::string::npos || relval_pos < secondary_pos))
      secondary_pos = relval_pos;
    // Check for prebello_
    size_t prebello_pos = text.find("prebello_", i);
    if (prebello_pos != std::string::npos && (secondary_pos == std::string::npos || prebello_pos < secondary_pos))
      secondary_pos = prebello_pos;
    if (secondary_pos != std::string::npos)
    {
      // Copy everything up to the secondary dataset path
      result += text.substr(i, secondary_pos - i);
      // Find the end of this secondary dataset path
      size_t path_end = secondary_pos;
      // Skip until we hit a character that's not part of the path (space, newline, etc.)
      while (path_end < text.length()
        && (false
          || std::isalnum(text[path_end])
          || text[path_end] == '_'
          || text[path_end] == '-'
          || text[path_end] == '/'
          || text[path_end] == '.'
        )
      )
      {
        path_end++;
      }
      // Continue from after the secondary dataset path
      i = path_end;
    }
    else
    {
      // No more secondary datasets found, copy the rest
      result += text.substr(i);
      break;
    }
  }
  return result;
}

inline std::string campaign_remove_cmsdriver_(const std::string& text)
{
  std::string result;
  result.reserve(text.length());
  size_t i = 0;
  while (i < text.length())
  {
    // Check if we find "cmsDriver.py" anywhere
    size_t cms_pos = text.find("cmsDriver.py", i);
    if (cms_pos != std::string::npos)
    {
      // Copy everything up to cmsDriver.py
      result += text.substr(i, cms_pos - i);
      // Find the end of this cmsDriver.py line
      size_t line_end = cms_pos;
      // Skip to the end of the line
      while (line_end < text.length() && text[line_end] != '\n' && text[line_end] != '\r')
      {
        line_end++;
      }
      // Skip the line ending too (\n or \r\n)
      if (line_end < text.length())
      {
        if (text[line_end] == '\r' && line_end + 1 < text.length() && text[line_end + 1] == '\n')
        {
          line_end += 2; // skip \r\n
        }
        else
        {
          line_end += 1; // skip \n or \r
        }
      }
      // Continue from after the cmsDriver.py line
      i = line_end;
    }
    else
    {
      // No more cmsDriver.py found, copy the rest
      result += text.substr(i);
      break;
    }
  }
  return result;
}

inline std::string campaign_exclude_url_(const std::string& text)
{
  std::string result;
  result.reserve(text.length());
  // Preprocessing: handle [front|http...] patterns - keep [front], remove |http...
  size_t i = 0;
  while (i < text.length())
  {
    if (text[i] == '[')
    {
      size_t bracket_start = i;
      size_t bracket_end = text.find(']', bracket_start + 1);
      if (bracket_end != std::string::npos)
      {
        // Check if there's a pipe inside the brackets
        size_t pipe_pos = text.find('|', bracket_start + 1);
        if (pipe_pos != std::string::npos && pipe_pos < bracket_end)
        {
          // Check if the part after | starts with http
          if (pipe_pos + 4 < text.length() && text.substr(pipe_pos + 1, 4) == "http")
          {
            // Extract and keep the front part (before |)
            std::string front_part = text.substr(bracket_start + 1, pipe_pos - bracket_start - 1);
            result += "[" + front_part + "]";
            i = bracket_end + 1;
            continue;
          }
        }
      }
    }
    result += text[i++];
  }
  // Now process the preprocessed text with existing logic
  std::string final_result;
  final_result.reserve(result.length());
  i = 0;
  while (i < result.length())
  {
    if (result[i] == '[')
    {
      size_t bracket_start = i;
      // Check for HTTP/HTTPS URLs: [http or [https
      if (i + 5 < result.length() && result.substr(i, 5) == "[http")
      {
        // Find the matching ']'
        size_t bracket_end = result.find(']', bracket_start + 1);
        if (bracket_end != std::string::npos)
        {
          // Skip the entire URL pattern including brackets
          i = bracket_end + 1;
          continue;
        }
      }
      // Check for [/...] patterns
      else if (i + 1 < result.length() && result[i + 1] == '/')
      {
        // Find the matching ']'
        size_t bracket_end = result.find(']', bracket_start + 1);
        if (bracket_end != std::string::npos)
        {
          // Skip the entire [/...] pattern including brackets
          i = bracket_end + 1;
          continue;
        }
      }
    }
    final_result += result[i++];
  }
  return final_result;
}

inline std::string campaign_expand_brackets_(const std::string& text)
{
  std::string result;
  result.reserve(text.length() * 2); // reserve space for expansion
  size_t i = 0;
  while (i < text.length())
  {
    if (text[i] == '[')
    {
      // Find the matching ']'
      size_t bracket_start = i;
      size_t bracket_end = text.find(']', bracket_start + 1);
      if (bracket_end == std::string::npos)
      {
        // No matching ']', just copy the '['
        result += text[i++];
        continue;
      }
      // Extract prefix (everything before '[')
      std::string prefix;
      size_t prefix_start = bracket_start;
      while (prefix_start > 0 && std::isalnum(text[prefix_start - 1]))
      {
        prefix_start--;
      }
      prefix = text.substr(prefix_start, bracket_start - prefix_start);
      // Extract content inside brackets
      std::string bracket_content = text.substr(bracket_start + 1, bracket_end - bracket_start - 1);
      // Extract suffix (alphanumeric characters after ']')
      std::string suffix;
      size_t suffix_end = bracket_end + 1;
      while (suffix_end < text.length() && std::isalnum(text[suffix_end]))
      {
        suffix += text[suffix_end];
        suffix_end++;
      }
      // Check if content is valid (only alphanumeric and |)
      bool valid_content = true;
      for (char c : bracket_content)
      {
        if (!std::isalnum(c) && c != '|')
        {
          valid_content = false;
          break;
        }
      }
      if (valid_content && !prefix.empty())
      {
        // Remove prefix from result (it was already copied before hitting '[')
        if (result.length() >= prefix.length())
        {
          result.erase(result.length() - prefix.length());
        }
        // Split by '|' and expand
        std::vector<std::string> parts;
        std::string current_part;
        for (char c : bracket_content)
        {
          if (c == '|')
          {
            if (!current_part.empty())
            {
              parts.push_back(current_part);
              current_part.clear();
            }
          }
          else if (std::isalnum(c))
          {
            current_part += c;
          }
        }
        if (!current_part.empty())
        {
          parts.push_back(current_part);
        }
        // Expand: prefixA[suffix] prefixB[suffix] prefixC[suffix] ...
        for (size_t j = 0; j < parts.size(); ++j)
        {
          if (j > 0) result += " ";
          result += prefix + parts[j] + suffix;
        }
        i = suffix_end;
      }
      else
      {
        // Invalid pattern, copy the entire bracket pattern as-is
        result += text.substr(bracket_start, suffix_end - bracket_start);
        i = suffix_end;
      }
    }
    else
    {
      result += text[i++];
    }
  }
  return result;
}

inline std::vector<std::string> campaign_words_(const std::string& text)
{
  std::vector<std::string> words;
  std::string current_word;
  for (char c : text)
  {
    if (std::isalnum(c)) current_word += c; // letter or number
    else
    {
      if (!current_word.empty())
      {
        words.push_back(current_word);
        current_word.clear();
      }
    }
  }
  if (!current_word.empty()) words.push_back(current_word);
  return words;
}

inline bool campaign_word_regex_(const std::string& word)
{
  if (word.length() <= 6) return false;
  if (!std::isupper(word[0]) && word.rfind("2022Commission", 0) != 0) return false;

  // Exclude common non-campaign words
  static const std::unordered_set<std::string> veto_words = {
    "NanoV15", "NanoAODv6", "NanoAODv9", "NanoAODv11", "NanoAODv12",
    "GEN-SIM", "DIGI-RAW", "MINIAODSIM", "AODSIM", "RECO", "RAW-RECO",
    "FlatPU", "HiMixNoPU", "NoPileUp", "PileUp", "MixingModule",
  };
  if (veto_words.find(word) != veto_words.end()) return false;

  // Exclude words starting with "Tune" or "Realistic"
  if (word.length() >= 4 && word.substr(0, 4) == "Tune") return false;
  if (word.length() >= 9 && word.substr(0, 9) == "Realistic") return false;
  if (word.length() >= 8) // Summer + 2 digits + at least 1 more char
  {
    if ((word.substr(0, 6) == "Summer" && word.length() >= 8 && std::isdigit(word[6]) && std::isdigit(word[7]))
      || (word.substr(0, 6) == "Winter" && word.length() >= 8 && std::isdigit(word[6]) && std::isdigit(word[7]))
      || (word.substr(0, 6) == "Spring" && word.length() >= 8 && std::isdigit(word[6]) && std::isdigit(word[7])))
    {
      return false; // "Summer23" or "Winter22" or "Spring24" etc.
    }
  }
  bool has_upper = false;
  bool has_lower = false;
  bool has_digit = false;
  for (char c : word)
  {
    if (!std::isalnum(c)) return false;
    if (std::isupper(c)) has_upper = true;
    if (std::islower(c)) has_lower = true;
    if (std::isdigit(c)) has_digit = true;
  }
  return has_upper && has_lower && has_digit;
}

inline std::string campaign_status_normalize_(const std::string& status)
{
  if (status.empty()) return "";
  std::string normalized = status;
  // trim starting whitespace
  normalized.erase(
    normalized.begin()
  , std::find_if(
      normalized.begin()
    , normalized.end()
    , [](char c)
      {
        return !std::isspace(static_cast<unsigned char>(c));
      }
    )
  );
  // trim trailing whitespace
  normalized.erase(
    std::find_if(
      normalized.rbegin()
    , normalized.rend()
    , [](char c)
      {
        return !std::isspace(static_cast<unsigned char>(c));
      }
    ).base()
  , normalized.end()
  );
  if (normalized.empty()) return "";
  // common variations
  if (normalized == "To Do" || normalized == "TODO" || normalized == "to do" || normalized == "todo") return "To Do";
  if (normalized == "Enabled" || normalized == "ENABLED" || normalized == "enabled") return "Enabled";
  if (normalized == "Open" || normalized == "OPEN" || normalized == "open") return "Open";
  if (normalized == "Closed" || normalized == "CLOSED" || normalized == "closed") return "Closed";
  return normalized;
}

inline int campaign_days_past_(const std::string& date_str)
{
  // 2025-01-29T17:15:47.000+0100
  std::tm tm = {};
  std::istringstream ss(date_str);
  ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
  if (ss.fail()) return 0;
  // YYYY-MM-DDTHH:MM:SS.mmm+HHMM or YYYY-MM-DDTHH:MM:SS.mmm-HHMM
  size_t tz_pos = date_str.find_last_of("+-");
  if (tz_pos == std::string::npos || tz_pos < 12) return 0;
  int offset_sign = (date_str[tz_pos] == '+') ? 1 : -1;
  int offset_hours = 0;
  int offset_minutes = 0;
  if (tz_pos + 5 <= date_str.length())
  {
    offset_hours = std::stoi(date_str.substr(tz_pos + 1, 2));
    offset_minutes = std::stoi(date_str.substr(tz_pos + 3, 2));
  }
  else if (tz_pos + 3 <= date_str.length())
  {
    offset_hours = std::stoi(date_str.substr(tz_pos + 1, 2));
  }
  int utc_offset_minutes = offset_sign * (offset_hours * 60 + offset_minutes);
  tm.tm_isdst = 0;
  std::time_t jira_time_t = timegm(&tm);
  if (jira_time_t == -1) return 0;
  // to UTC
  jira_time_t -= utc_offset_minutes * 60;
  auto jira_time = std::chrono::system_clock::from_time_t(jira_time_t);
  auto now = std::chrono::system_clock::now();
  auto diff = std::chrono::duration_cast<std::chrono::hours>(now - jira_time);
  int days = diff.count() / 24;
  return (days < 0) ? 0 : days;
}

inline std::vector<std::string> campaign_potentials_(const std::unordered_set<std::string>& words_set)
{
  std::vector<std::string> candidates;
  for (const auto& word : words_set)
  {
    if (campaign_word_regex_(word)) candidates.push_back(word);
  }
  return candidates;
}

inline std::vector<std::pair<std::string, uint8_t>> campaign_names_(const jira_s& jira_entry
  , const std::unordered_map<std::string, uint8_t>& campaign_status
) // v<name, status>
{
  std::vector<std::pair<std::string, uint8_t>> campaigns;
  std::string combined_text = jira_entry.summary + " " + jira_entry.description;
  // preprocess: exclude URL patterns like [https://...]
  std::string cleaned_text = campaign_exclude_url_(combined_text);
  // preprocess: expand bracket patterns like "prefix[A|B|C]" to "prefixA prefixB prefixC"
  std::string expanded_text = campaign_expand_brackets_(cleaned_text);
  std::vector<std::string> words_orig = campaign_words_(expanded_text); // original expanded to search in JSON
  // preprocess: remove cmsDriver.py command lines
  std::string text_no_cmsdriver = campaign_remove_cmsdriver_(expanded_text);
  // preprocess: remove secondary dataset paths
  std::string text_no_secondary = campaign_remove_secondary_(text_no_cmsdriver);
  std::vector<std::string> words = campaign_words_(text_no_secondary);
  std::unordered_set<std::string> seen_campaigns;
  std::unordered_set<std::string> words_set(words.begin(), words.end());
  // first pass: exact matches in json
  for (const auto& word : words_orig)
  {
    if (campaign_status.find(word) != campaign_status.end() && seen_campaigns.find(word) == seen_campaigns.end())
    {
      campaigns.push_back(std::make_pair(word, campaign_status.at(word)));
      seen_campaigns.insert(word);
      words_set.erase(word);
    }
  }
  // second pass: check for potential new campaigns
  std::vector<std::string> candidates = campaign_potentials_(words_set);
  for (const std::string& candidate : candidates)
  {
    campaigns.push_back(std::make_pair(candidate, 2)); // status 2 = from JIRA and not in json
  }
  return campaigns;
}

inline std::string campaign_status_should_be_(const std::vector<std::pair<std::string, uint8_t>>& campaign_status
  , const std::string& time_created
)
{
  if (campaign_status.empty()) return "Closed";
  bool has_jira_only = false;
  bool has_disabled = false;
  bool has_enabled = false;
  for (const auto& pair : campaign_status)
  {
    if (pair.second == 2) has_jira_only = true;
    else if (pair.second == 0) has_disabled = true;
    else if (pair.second == 1) has_enabled = true;
  }
  if (has_disabled) return "Open"; // one in-JSON disabled campaign sets Open
  if (has_enabled) return "Enabled"; // one in-JSON enabled campaign with no in-JSON disabled campaigns sets Enabled
  if (has_jira_only)
  {
    if (campaign_days_past_(time_created) >= 60) return "Closed";
    else return "To Do";
  }
  else return "None";
}

inline void campaign_veto_abbreviations_(std::unordered_map<std::string, std::vector<std::pair<std::string, uint8_t>>>& jira_campaigns)
{
  for (auto& entry : jira_campaigns)
  {
    std::vector<std::pair<std::string, uint8_t>>& campaigns = entry.second;
    // Find JIRA_ONLY campaigns (status == 2) and JSON campaigns
    std::vector<std::string> jira_only_names;
    std::vector<std::pair<std::string, uint8_t>> json_campaigns;
    for (const auto& camp_pair : campaigns)
    {
      if (camp_pair.second == 2) jira_only_names.push_back(camp_pair.first);
      else json_campaigns.push_back(camp_pair);
    }
    // Check each JIRA_ONLY campaign individually
    if (!jira_only_names.empty() && !json_campaigns.empty())
    {
      std::vector<std::string> to_remove;
      // For each JIRA_ONLY campaign, check if it's contained in ALL JSON campaign names
      for (const std::string& jira_only_name : jira_only_names)
      {
        bool is_contained_in_all = true;
        for (const auto& json_camp : json_campaigns)
        {
          if (json_camp.first.find(jira_only_name) == std::string::npos)
          {
            is_contained_in_all = false;
            break;
          }
        }
        // If it's contained in all JSON names, mark it for removal
        if (is_contained_in_all)
        {
          to_remove.push_back(jira_only_name);
        }
      }
      // Remove all marked JIRA_ONLY campaigns
      if (!to_remove.empty())
      {
        campaigns.erase(
          std::remove_if(
            campaigns.begin()
          , campaigns.end()
          , [&to_remove](const std::pair<std::string, uint8_t>& p)
            {
              if (p.second == 2)
              {
                for (const std::string& name : to_remove)
                {
                  if (p.first == name) return true;
                }
              }
              return false;
            }
          )
        , campaigns.end()
        );
      }
    }
  }
}

#endif
